package cn.vconinfo.base.redis;

import cn.vconinfo.base.utils.CommonUtils;
//import io.lettuce.core.SetArgs;
//import io.lettuce.core.api.async.RedisAsyncCommands;
//import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 *  利用redis的分布式锁
 * 
 * @author oms
 * @version $Id: RedisLock.java, v 0.1 2020年2月6日 上午9:31:04 oms Exp $
 */
@Component
@Slf4j
public class RedisLock {
    private static final String SUCCESS = "OK";
    private static final String LOCK_KEY_FORMAT = "lock:{}:{}";
    private static final String LUA_SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

    private static StringRedisTemplate redisTemplate;
    
    @PostConstruct
    public void init() {
        redisTemplate.setEnableTransactionSupport(true);
    }

    @Autowired
    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        RedisLock.redisTemplate = redisTemplate;
    }

    private RedisLock() {
    }


    public static <T> String getLockKey(@Nonnull String business, @Nonnull T resource) {
        return CommonUtils.format("lock:{}:{}", new Object[]{business, resource});
    }

    @Nullable
    public static String tryLock(@Nonnull String lockKey, long expireSeconds) {
        String uuid = CommonUtils.getUuid();
        boolean result = tryLock(lockKey, uuid, expireSeconds);
        return result ? uuid : null;
    }

    public static boolean tryLock(@Nonnull String lockKey, @Nonnull String lockValue, long expireSeconds) {
        try {
            String result = (String)redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
//                    JedisCommands commands = (JedisCommands)connection.getNativeConnection();
//                    return commands.set(lockKey, lockValue, "NX", "EX", expireSeconds);
                    Object nativeConnection = connection.getNativeConnection();
                    String status = null;
                    RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();

                    byte[] keyByte = stringRedisSerializer.serialize(lockKey);
                    //springboot 2.0以上的spring-data-redis 包默认使用 lettuce连接包

//                    //lettuce连接包，集群模式，ex为秒，px为毫秒
//                    if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
//                        log.debug("lettuce Cluster:---setKey:"+lockKey+"---value"+lockValue+"---maxTimes:"+expireSeconds);
//                        status = ((RedisAdvancedClusterAsyncCommands) nativeConnection)
//                                .getStatefulConnection().sync()
//                                .set(keyByte,keyByte, SetArgs.Builder.nx().ex(expireSeconds));
//                        log.debug("lettuce Cluster:---status:"+status);
//                    }
//                    //lettuce连接包，单机模式，ex为秒，px为毫秒
//                    if (nativeConnection instanceof RedisAsyncCommands) {
//                        log.debug("lettuce single:---setKey:"+ lockKey+"---value"+lockValue+"---maxTimes:"+expireSeconds);
//                        status = ((RedisAsyncCommands) nativeConnection)
//                                .getStatefulConnection().sync()
//                                .set(keyByte,keyByte, SetArgs.Builder.nx().ex(expireSeconds));
//                        log.debug("lettuce single:---status:"+status);
//                    }
                    return status;
                }
            });
            return "OK".equals(result);
        } catch (Exception ex) {
            log.error("tryLock error:", ex);
            return false;
        }
    }

    public static String lock(@Nonnull String lockKey, long expireSeconds, int tryTimes, long sleepMillis) {
        String uuid = CommonUtils.getUuid();
        boolean result = lock(lockKey, uuid, expireSeconds, tryTimes, sleepMillis);
        return result ? uuid : null;
    }

    public static boolean lock(@Nonnull String lockKey, @Nonnull String lockValue, long expireSeconds, int tryTimes, long sleepMillis) {
        boolean result = false;
        int count = 0;

        try {
            do {
                ++count;
                result = tryLock(lockKey, lockValue, expireSeconds);
                if (!result) {
                    TimeUnit.MILLISECONDS.sleep(sleepMillis);
                }
            } while(!result && count <= tryTimes);
        } catch (InterruptedException ex) {
            log.error(ex.getMessage(), ex);
        }

        return result;
    }

    public static void unlock(@Nonnull String lockKey, @Nonnull String lockValue) {
        try {
            redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    if (nativeConnection instanceof Jedis) {
                        return (Long)((Jedis)nativeConnection).eval("if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end", 1, new String[]{lockKey, lockValue});
                    } else {
                        return nativeConnection instanceof JedisCluster ? (Long)((JedisCluster)nativeConnection).eval("if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end", 1, new String[]{lockKey, lockValue}) : 0L;
                    }
                }
            });
        } catch (Exception ex) {
            log.error("unlock error:", ex);
        }

    }

}
